import { DependencyList, useCallback, useState, useRef } from 'react';
import useMountedState from './useMountedState';

type FnReturningPromise = (...args: any[]) => Promise<any>;
type PromiseType<P extends Promise<any>> = P extends Promise<infer T>
  ? T
  : never;

type AsyncState<T> =
  | {
    loading: boolean;
    error?: undefined;
    value?: undefined;
  }
  | {
    loading: true;
    error?: Error | undefined;
    value?: T;
  }
  | {
    loading: false;
    error: Error;
    value?: undefined;
  }
  | {
    loading: false;
    error?: undefined;
    value: T;
  };

type StateFromFnReturningPromise<T extends FnReturningPromise> = AsyncState<
PromiseType<ReturnType<T>>
>;

type AsyncFnReturn<T extends FnReturningPromise = FnReturningPromise> = [
  StateFromFnReturningPromise<T>,
  T,
];

/* eslint-disable */
export function useAsyncFn<T extends FnReturningPromise>(
  fn: T,
  deps: DependencyList = [],
  initialState: StateFromFnReturningPromise<T> = { loading: false },
): AsyncFnReturn<T> {
  const lastCallId = useRef(0);
  const isMounted = useMountedState();
  const [state, set] = useState<StateFromFnReturningPromise<T>>(initialState);

  const callback = useCallback((...args: Parameters<T>): ReturnType<T> => {
    const callId = ++lastCallId.current;
    set(prevState => ({ ...prevState, loading: true }));

    return fn(...args).then(
      (value) => {
        isMounted()
          && callId === lastCallId.current
          && set({ value, loading: false });

        return value;
      },
      (error) => {
        isMounted()
          && callId === lastCallId.current
          && set({ error, loading: false });

        return error;
      },
    ) as ReturnType<T>;
  }, deps);

  return [state, (callback as unknown) as T];
}
